#
# Copyright (c) 2023 supercell
#
# SPDX-License-Identifier: BSD-3-Clause
#

module Luce
  # Parses tables.
  class TableSyntax < BlockSyntax
    def can_end_block?(parser : BlockParser) : Bool
      true
    end

    def pattern : Regex
      Luce.dummy_pattern
    end

    def can_parse?(parser : BlockParser) : Bool
      parser.matches_next? Luce.table_pattern
    end

    # Parses a table into its three parts:
    #
    # * a head row of head cells (`<th>` cells)
    # * a divider of hyphens and pipes (not rendered)
    # * many body rows of body cells (`<td>` cells)
    def parse(parser : BlockParser) : Node?
      # ameba:disable Lint/NotNil
      alignments = parse_alignments(parser.next.not_nil!.content)
      column_count = alignments.size
      head_row = parse_row(parser, alignments, "th")
      # ameba:disable Lint/NotNil
      if head_row.children.not_nil!.size != column_count
        parser.retreat
        return nil
      end

      head = Element.new("thead", [head_row.as Node])

      # Advance past the divider of hyphens
      parser.advance

      rows = [] of Node
      until parser.done? || BlockSyntax.at_block_end?(parser)
        row = parse_row(parser, alignments, "td")
        children = row.children
        if !children.nil?
          while children.size < column_count
            # Insert synthetic empty cells.
            children << Element.new("td", [] of Node)
          end
          while children.size > column_count
            children.pop
          end
        end
        # ameba:disable Lint/NotNil
        while row.children.not_nil!.size > column_count
          # ameba:disable Lint/NotNil
          row.children.not_nil!.pop
        end
        rows << row
      end
      if rows.empty?
        return Element.new("table", [head.as Node])
      end

      body = Element.new("tbody", rows)
      Element.new("table", [head.as Node, body.as Node])
    end

    private def parse_alignments(line : String) : Array(String?)
      columns = [] of String?
      # Set the value to `true` when hitting a non whitespace character other
      # than the first pipe character.
      started = false
      hit_dash = false
      alignment : String? = nil

      line.each_codepoint do |char|
        next if char == Charcode::SPACE || char == Charcode::TAB || (!started && char == Charcode::PIPE)
        started = true

        if char == Charcode::COLON
          if hit_dash
            alignment = alignment == "left" ? "center" : "right"
          else
            alignment = "left"
          end
        end

        if char == Charcode::PIPE
          columns << alignment
          hit_dash = false
          alignment = nil
        else
          hit_dash = true
        end
      end

      columns << alignment if hit_dash

      columns
    end

    # Parses a table row at the current line into a table row element, with
    # parsed table cells.
    #
    # *alignments* is used to annotate an alignment on each cell, and
    # *cell_type* is used to declare either "td" or "th" cells.
    private def parse_row(parser : BlockParser, alignments : Array(String?), cell_type : String) : Element
      line = parser.current
      cells = [] of String
      index = walk_past_opening_pipe(line.content)
      cell_buffer = String::Builder.new

      loop do
        if index >= line.content.size
          # This row has ended without a trailing pipe, which is fine.
          cells << cell_buffer.to_s.rstrip
          break
        end
        ch = line.content.codepoint_at(index)
        if ch == Charcode::BACKSLASH
          if index == line.content.size - 1
            # A table row ending in a backslash is not well-specified, but it
            # looks like GitHub just allows the character as part of the text of
            # the last cell.
            cell_buffer << ch.chr
            cells << cell_buffer.to_s.rstrip
            break
          end
          escaped = line.content.codepoint_at(index + 1)
          if escaped == Charcode::PIPE
            # GitHub Flavored Markdown has a strange bit here; the pipe is to be
            # escaped before any other inline processing. One consequence, for
            # example, is that "| `\|` |" should be parsed as a cell with a code
            # element with text "|", rather than "\|". Most parsers are not
            # compliant with this corner, but this is what is specified, and what
            # GitHub does in practice.
            cell_buffer << escaped.chr
          else
            # The `InlineParser` will handle the escaping
            cell_buffer << ch.chr
            cell_buffer << escaped.chr
          end
          index += 2
        elsif ch == Charcode::PIPE
          cells << cell_buffer.to_s.rstrip
          cell_buffer = String::Builder.new
          # Walk forward past any whitespace which leads to the next cell
          index += 1
          index = walk_past_whitespace(line.content, index)
          # This row ended with a trailing pipe
          break if index >= line.content.size
        else
          cell_buffer << ch.chr
          index += 1
        end
      end
      parser.advance
      row = cells.map { |cell| Element.new(cell_type, [UnparsedContent.new(cell).as Node]).as Node }

      i = 0
      while i < row.size && i < alignments.size
        if alignments[i].nil?
          i += 1
          next
        end

        (row[i].as Element).attributes["align"] = "#{alignments[i]}"
        i += 1
      end

      Element.new("tr", row)
    end

    # Walks past whitespace in *line* starting at *index*.
    #
    # Returns the index of the first non-whitespace character.
    private def walk_past_whitespace(line : String, index : Int32) : Int32
      while index < line.size
        ch = line.codepoint_at(index)
        if ch != Charcode::SPACE && ch != Charcode::TAB
          break
        end
        index += 1
      end

      index
    end

    # Walks past the opening pipe (and any whitespace that surronds it) in
    # *line*.
    #
    # Returns the index of the first non-whitespace character after the pipe.
    # If no opening pipe is found, this just returns the index of the first
    # non-whitespace character.
    private def walk_past_opening_pipe(line : String) : Int32
      index = 0
      while index < line.size
        ch = line.codepoint_at(index)
        if ch == Charcode::PIPE
          index += 1
          index = walk_past_whitespace(line, index)
        end
        if ch != Charcode::SPACE && ch != Charcode::TAB
          # No leading pipe
          break
        end
        index += 1
      end

      index
    end
  end
end
